# -*- coding: utf-8 -*-

import os
import re
import javalang
from typing import List
from typing import Dict
from typing import Set

from javalang.tree import Annotation, CompilationUnit, ClassDeclaration, EnumBody, MethodDeclaration
from conf.entity.module.grpc_config import GrpcAndDomainConverterConfig
from com.maycur.grpc.base.base_entity import java_utils_dict


class JavaImportInfo(object):
    path: str
    line_idx: int


class JavaFieldInfo(object):
    name: str
    type: str
    list_type: str
    # line index in class
    line_idx: int


class JavaMethodInfo(object):
    name: str
    # method declaration
    declaration: MethodDeclaration
    # first line index in class
    f_line_idx: int
    # last line index in class
    l_line_idx: int


class JavaClassInfo(object):
    lines: List[str]
    simple_name: str
    full_name: str
    package: str
    path: str
    logger: JavaFieldInfo
    imports: List[JavaImportInfo]
    import_dict: Dict[str, JavaImportInfo]
    annotation_set: Set[str]
    raw_annotation_list: List[Annotation]
    fields: List[JavaFieldInfo]
    field_dict: Dict[str, JavaFieldInfo]
    methods: List[JavaMethodInfo]
    method_dict: Dict[str, JavaMethodInfo]
    class_close_idx: int
    has_lombok_data: bool
    has_lombok_builder: bool
    has_lombok_slf4j: bool
    has_big_decimal: bool
    is_exist: bool

    def __init__(self, is_exist=True):
        self.lines = []
        self.imports = []
        self.import_dict = {}
        self.annotation_set = set()
        self.raw_annotation_list = []
        self.fields = []
        self.field_dict = {}
        self.methods = []
        self.method_dict = {}
        self.class_close_idx = -1
        self.has_lombok_data = False
        self.has_lombok_builder = False
        self.has_lombok_slf4j = False
        self.has_big_decimal = False
        self.is_exist = is_exist
        self.logger = None

    def set_simple_name(self, class_name: str):
        if '/' in class_name:
            class_name = class_name[class_name.rindex('/'):]
        if class_name.endswith('.java'):
            class_name = class_name.replace('.java', '')
        self.simple_name = class_name

    def collect_imports(self, class_tree: CompilationUnit):
        if class_tree.imports is not None and len(class_tree.imports) > 0:
            for item in class_tree.imports:
                if item.static:
                    # skip static import, such as Constant field
                    continue
                import_info = JavaImportInfo()
                import_info.path = item.path
                import_info.line_idx = item.position.line
                self.imports.append(import_info)
                self.import_dict[import_info.path] = import_info

                if class_tree.types[0].annotations is not None:
                    for annotation_item in class_tree.types[0].annotations:
                        # check lombok @Data
                        if 'lombok.Data' in import_info.path and annotation_item.name == 'Data':
                            self.has_lombok_data = True
                        # check lombok @Builder
                        if 'lombok.Builder' in import_info.path and annotation_item.name == 'Builder':
                            self.has_lombok_builder = True

                        # check lombok @slf4j
                        if java_utils_dict['Slf4j'] in import_info.path and annotation_item.name == 'Slf4j':
                            log = JavaFieldInfo()
                            log.name = 'log'
                            log.type = 'org.slf4j.Logger'
                            self.logger = log
                            self.has_lombok_slf4j = True

    def collect_annotations(self, class_tree: CompilationUnit):
        if class_tree.types[0].annotations is not None:
            self.raw_annotation_list = class_tree.types[0].annotations
            for item in self.raw_annotation_list:
                self.annotation_set.add(item.name)

    def collect_fields(self, class_declaration: ClassDeclaration):
        if class_declaration.fields is not None and len(class_declaration.fields) > 0:
            for item in class_declaration.fields:
                field_info = JavaFieldInfo()
                field_info.name = item.declarators[0].name
                field_info.type = item.type.name
                field_info.line_idx = item.position.line
                self.fields.append(field_info)
                self.field_dict[field_info.name] = field_info

                if 'List' == field_info.type:
                    field_info.list_type = item.type.arguments[0].type.name
                elif 'Map' in field_info.type:
                    print(f'map field found, the field: {field_info.name}')

                # check BigDecimal
                elif 'BigDecimal' == field_info.type:
                    self.has_big_decimal = True
                elif 'Logger' == field_info.type:
                    self.logger = field_info

    def collect_methods(self, class_declaration: ClassDeclaration):
        if class_declaration.methods is not None and len(class_declaration.methods) > 0:
            m_len = len(class_declaration.methods)
            for idx in range(m_len):
                item = class_declaration.methods[idx]
                method_info = JavaMethodInfo()
                method_info.name = item.name
                method_info.declaration = item
                method_info.f_line_idx = item.position.line
                method_info.l_line_idx = item.position.line

                # abstract methods have no body
                if item.body is not None:
                    body_len = len(item.body)
                    if body_len > 0:
                        # determine where the body start is
                        start_line = item.position.line - 1
                        lines_len = len(self.lines)
                        while start_line < lines_len:
                            # find the first "{" of the method
                            if '{' in self.lines[start_line]:
                                method_info.f_line_idx = start_line + 1
                                break
                            else:
                                start_line += 1
                        # determine where the body ending is
                        # javalang do not support Java8 lambda expression
                        if idx == m_len - 1:
                            # last method of the class
                            class_end_bracket_found = False
                            for idx_1 in range(lines_len - 1, -1, -1):
                                if '}' in self.lines[idx_1]:
                                    if not class_end_bracket_found:
                                        class_end_bracket_found = True
                                    else:
                                        method_info.l_line_idx = idx_1
                                        break
                        else:
                            next_m = class_declaration.methods[idx + 1]
                            next_m_line = next_m.position.line
                            for idx_2 in range(next_m_line, item.position.line, -1):
                                try:
                                    if '}' in self.lines[idx_2]:
                                        method_info.l_line_idx = idx_2
                                        break
                                except Exception as e:
                                    print(e)

                self.methods.append(method_info)
                # method overloading TODO
                self.method_dict[method_info.name] = method_info


class JavaLoader(object):
    """
    JavaLoader - loader to load Java class
    """

    classes: List[JavaClassInfo]
    path_dict: Dict[str, JavaClassInfo]
    fullname_dict: Dict[str, JavaClassInfo]
    simple_name_dict: Dict[str, JavaClassInfo]
    enum_dict: Dict[str, JavaClassInfo]
    sub_package_set: Set[str]
    all_commented_classes: List[str]

    def __init__(self):
        self.classes = []
        self.path_dict = {}
        self.fullname_dict = {}
        self.simple_name_dict = {}
        self.enum_dict = {}
        self.sub_package_set = set()
        self.all_commented_classes = []

    def load_by_class(self, class_path: str, class_name: str):
        """
        load class
        :param class_path: parent directory of the class
        :param class_name: the class name
        :return:
        """
        path = f'{class_path}/{class_name}'
        if path in self.path_dict:
            print(f'the class is already loaded: {path}')
            return

        class_info = JavaClassInfo()
        class_info.path = path
        class_info.set_simple_name(class_name)

        with open(class_info.path, 'r') as class_f:
            class_info.lines = class_f.readlines()
            # prepare class code for javalang parser
            lines_str = ''
            for idx, line in enumerate(class_info.lines):
                trim_line = line.strip()
                if trim_line == '}':
                    class_info.class_close_idx = idx
                lines_str += line

            try:
                # Use javalang to parse class elements
                class_tree = javalang.parse.parse(lines_str)

                if class_tree.package is None:
                    # 已经被注释掉的类，没有package语句
                    self.all_commented_classes.append(class_name)
                    return
                class_info.package = class_tree.package.name
                class_info.full_name = class_info.package + '.' + class_info.simple_name
                class_info.collect_imports(class_tree)
                class_info.collect_annotations(class_tree)
                class_info.collect_fields(class_tree.types[0])
                class_info.collect_methods(class_tree.types[0])

                self.classes.append(class_info)
                self.path_dict[class_info.path] = class_info
                self.fullname_dict[class_info.full_name] = class_info
                self.simple_name_dict[class_info.simple_name] = class_info

                self.collect_inner_classes_and_enums(class_tree.types[0], class_info.package)
            except Exception as e:
                print(e)

    def load_by_package(self, package_path: str):
        if not os.path.exists(package_path):
            print(f'package not found, the package: {package_path}')
            return
        if not os.path.isdir(package_path):
            print(f'package is not a directory, the package: {package_path}')
            return
        for item in os.listdir(package_path):
            package_dir = f'{package_path}/{item}'
            if os.path.isdir(package_dir):
                self.sub_package_set.add(item)
                self.load_by_package(package_dir)
            elif item.endswith('.java'):
                self.load_by_class(package_path, item)

    def load_import_references(self, converter_config: GrpcAndDomainConverterConfig):
        to_be_loaded_list = []
        for class_item in self.classes:
            for item in class_item.import_dict:
                if converter_config.root_package in item:
                    if item[item.rindex('.') + 1:] not in self.simple_name_dict:
                        to_be_loaded_list.append(item)
        if len(to_be_loaded_list) > 0:
            for item in to_be_loaded_list:
                item_package = item[:item.rindex('.')]
                item_name = item[item.rindex('.') + 1:] + '.java'
                class_path = f'{converter_config.project_path}/{converter_config.root_path}'
                class_path += f'{item_package.replace(converter_config.root_package, "").replace(r".", "/")}'
                class_path = re.sub('/+', '/', class_path)
                try:
                    self.load_by_class(class_path, item_name)
                except FileNotFoundError as e:
                    print(e)

    def collect_enums(self, class_declaration: ClassDeclaration):
        class_name = class_declaration.name
        class_item = self.simple_name_dict[class_name]
        self.enum_dict[class_name] = class_item

    def collect_inner_classes_and_enums(self, class_declaration: ClassDeclaration, package: str):
        if type(class_declaration.body) == EnumBody:
            self.collect_enums(class_declaration)
            return
        if len(class_declaration.body) > 0:
            for item in class_declaration.body:
                if isinstance(item, ClassDeclaration):
                    class_info = JavaClassInfo()
                    class_info.simple_name = item.name
                    class_info.collect_fields(item)
                    class_info.collect_methods(item)
                    # wrapper class name of the inner class, for import usage
                    class_info.package = f'{package}.{class_declaration.name}'

                    self.classes.append(class_info)
                    self.simple_name_dict[class_info.simple_name] = class_info
